package org.dei.perla.utils.dataconverter;

import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;

import org.dei.perla.utils.dataconverter.enums.Endianness;
import org.dei.perla.utils.dataconverter.enums.Sign;

public abstract class ObjectMarshaler<T> {

	public abstract Byte[] marshal(T converted, Byte[] toConvertByteArray);
	
	public abstract Byte[] unmarshal(T toConvert);
	
	/**
	 * Crea un oggetto di classe typeClass e lo inizializza con i Byte passati come parametro.
	 * I Bytes utilizzati vengono consumati (eliminati dall'ArrayList passata come parametro)
	 * @param typeClass classe dell'oggetto da creare
	 * @param toConvertByteArrayList ArrayList contenente i Bytes da utilizzare per l'inizializzazione
	 * 		  dell'oggetto da istanziare
	 * @param size dimensione del campo
	 * @param sign segno del SimpleField da convertire
	 * @param endianness Formato dei dati nell'ArrayList di Byte
	 * @return Oggetto di tipo typeClass inizializzato a seconda dei dati contenuti nell'annotazione ed
	 * 		   i valori presenti nell'ArrayList di Byte
	 * @throws IllegalArgumentException
	 */
	protected <V> V simpleFieldFromByteArray(Class<V> typeClass, ArrayList<Byte> toConvertByteArrayList,
			int size, Sign sign, Endianness endianness) throws IllegalArgumentException {
		Byte[] byteArray = new Byte[size];
		try {
			byteArray = toConvertByteArrayList.subList(0, size).toArray(byteArray);
		} catch (IndexOutOfBoundsException e) {
			throw new IllegalArgumentException("L'array di byte passato come parametro non contiene abbastanza " +
					"elementi per riempire la struttura richiesta!");
		}
		V converted = loadNumberFromByteArray(byteArray, typeClass, endianness, sign);
		toConvertByteArrayList.subList(0, size).clear();
		return converted;
	}
	
	/**
	 * Converte un array di Byte in un numero
	 * @param byteArray array di byte da convertire
	 * @param typeClass Classe del tipo in cui l'array di Byte dovra' essere caricato
	 * @param endianness Formato in cui i dati sono stati memorizzati nell'array
	 * @param sign segno del SimpleField da convertire
	 * @return Numero inizializzato secondo il valore contenuto nell'array di Byte
	 */
	private <V> V loadNumberFromByteArray(Byte[] byteArray, Class<V> typeClass, Endianness endianness, Sign sign) {
		BigInteger number;
		
		if (endianness == Endianness.LITTLE_ENDIAN) {
			byteArray = ArrayUtility.reverseArray(byteArray);
		}
		if (sign == Sign.UNSIGNED){
			number = new BigInteger(1, ArrayUtility.toPrimitiveByteArray(byteArray));
		}
		else
			number = new BigInteger(ArrayUtility.toPrimitiveByteArray(byteArray));
		
		return convertToSpecificNumber(number, typeClass);
	}
	
	/**
	 * Converte un numero BigInteger in un determinato tipo. Nel caso il tipo di destinazione
	 * non sia bbastanza grande per contenere il numero si verifichera' una perdita di dati
	 * @param toConvert BigInteger rappresentante il numero da convertire nel tipo adatto
	 * @param typeClass classe in cui si vuole che il numero sia convertito
	 * @return numero rappresentato con la classe richiesta
	 */
	private <V> V convertToSpecificNumber(BigInteger toConvert, Class<V> typeClass) throws RuntimeException {
		if (typeClass == Byte.class || typeClass == byte.class) {
			@SuppressWarnings("unchecked")
			V toReturn =  (V) new Byte(toConvert.byteValue());
			return toReturn;
		} else if (typeClass == Short.class || typeClass == short.class) {
			@SuppressWarnings("unchecked")
			V toReturn = (V) new Short(toConvert.shortValue());
			return toReturn;
		} else if (typeClass == Integer.class || typeClass == int.class) {
			@SuppressWarnings("unchecked")
			V toReturn = (V) new Integer(toConvert.intValue());
			return toReturn;
		} else if (typeClass == Long.class || typeClass == long.class) {
			@SuppressWarnings("unchecked")
			V toReturn = (V) new Long(toConvert.longValue());
			return toReturn;
		} else if (typeClass == BigInteger.class) {
			@SuppressWarnings("unchecked")
			V toReturn = (V) toConvert;
			return toReturn;
		} else {
			throw new RuntimeException(this.getClass() + ".convertToSpecificNumer: " +
					"impossibile convertire, " + typeClass.getName() + "non e' una sottoclasse di Number!");
		}
	}
	
	/**
	 * Converte il l'oggetto passato come parametro in un ArrayList<Byte>
	 * Il metodo restituisce null nel caso il tipo di oggetto da convertire non sia supportato.
	 * Il numero sara' troncato nel caso la sua rappresentazione necessiti piu' bytes
	 * di quanti specificati con il parametro size.
	 * @param toConvert numero da convertire
	 * @param endianness ordine dei byte nell'array
	 * @param size numero di bytes da utilizzare per la conversione
	 * @return ArrayList<Byte> rappresentante il numero convertito
	 */
	protected ArrayList<Byte> storeNumberToByteArray(Object toConvert,
			Endianness endianness, int size) throws RuntimeException{
		ArrayList<Byte> byteArray = null;
		
		if (toConvert instanceof Number) {
			BigInteger number = null;
			try {
				number = new BigInteger(toConvert.toString());
			} catch (NumberFormatException e) {
				throw new RuntimeException(this.getClass() + ".storeNumberToByteArray: " +
						"impossibile convertire, " + toConvert.getClass() + "non e' istanziabile" +
								"come BigInteger!");
			}
			ArrayList<Byte> tmpByteArray = new ArrayList<Byte>();
			
			tmpByteArray.addAll(Arrays.asList(ArrayUtility.toByteClassArray(number.toByteArray())));
			int excessBytes = size - tmpByteArray.size();
			
			if (excessBytes > 0) {
				byteArray = new ArrayList<Byte>(size);
				if (number.compareTo(BigInteger.ZERO) < 0) {
					byteArray.addAll(Collections.nCopies(excessBytes, new Byte("-1")));
				} else {
					byteArray.addAll(Collections.nCopies(excessBytes, new Byte("0")));
				}		
				byteArray.addAll(tmpByteArray);		
			} else if (excessBytes < 0) {
				byteArray = new ArrayList<Byte>(size);
				byteArray.addAll(tmpByteArray.subList(-1 * excessBytes, tmpByteArray.size()));
			} else {
				byteArray = tmpByteArray; 
			}
		}
		
		if (byteArray != null && endianness == Endianness.LITTLE_ENDIAN) {
			Collections.reverse(byteArray);
		}
		
		return byteArray;
	}
}
